
<html>
<head>
<script src="./js/live2dcubismcore.min.js"></script>
<script src="./js/live2d.min.js"></script>
<script src="./js/pixi.min.js"></script>

<!-- if only Cubism 4 support-->
<script src="./js/cubism4.min.js"></script>

<script src="./js/jquery-3.1.1.min.js"></script>

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

</head>

<body>
<canvas id=canvas></canvas>

<div id="control">

  <button id="play">测试音频</button> 
  <br />
  <label>数字人模型</label>
 <select id="model_list"></select> <button id="update_model">更新模型</button>
 <br />
  <label>眼神跟随鼠标</label>
  <input type="radio"  name="eyes" value="true" checked >
  <label>跟随鼠标</label><input type="radio" id="option2" name="eyes" value="false" ><label >前方直视</label>
  <br />
 <label>背景控制</label>
 <input type="radio" id="option1" name="options" value="bg_color" checked >
 <label for="option1">背景颜色</label><input id="bg_color" type="text" style="width:100px;"  ><br /><input type="radio" id="option2" name="options" value="bg_img" ><label for="option2">背景图片</label> <input type="file" id="imgupload" style="display:none"/>
 <button id="openImgUpload">上传图片</button> <button id="update_bg">更新背景</button>
 <br />
  <label>语音接口地址</label>
  <input type="search" id="apiurl" style="width:200px;" value="http://127.0.0.1:9880" >
  <br />
  <label>推理文本语言种类</label>
  <input type="search" id="text_lang" style="width:200px;" value="zh" >
  <br />
  <label>参考音频</label>
  <input type="search" id="ref_audio_path" style="width:200px;" value="./Keira.wav" >
  <br />
  <label>参考音频文本</label>
  <input type="search" id="prompt_text" style="width:300px;" value="光动嘴不如亲自做给你看,等我一下呀" >
  <br />
  <label>参考音频文本语种</label>
  <input type="search" id="prompt_lang" style="width:200px;" value="zh" >
  <br />
  <label>切分方式</label>
  <input type="search" id="text_split_method" style="width:200px;" value="cut5" >
  <br />
  <label>语速</label>
  <input type="search" id="speed_factor" style="width:200px;" value="1.0" >
  <br />

  <br />
  <label>ollama接口地址</label>
  <input type="search" id="llmurl" style="width:300px;" value="http://localhost:11434/api/chat" >

  <label>模型名称</label>
  <input id="model_name"  value="nsfw"  >
  <br />
  <label>引导词(以AI的视角)</label>
  <input id="role_card" style="width:300px;" value="亲爱的，我是你的性感女友，我会为了你做任何事情。"> 
  <br />
  <label>对话长度</label><input id="tokens" value="50"> <label>temperature温度</label><input id="temperature" value="0.5">
  <br />
  <label>聊天文本</label>
  <input id="text" style="width:300px;" value=""> <button id="start_talk">发送消息</button> 

  <br /><br />

  <textarea id="text_talk" style="width:400px;height:200px;font-size:15px;"></textarea>

  <button id="start_stream" >流式合成语音</button>  <button id="start">并行合成语音</button>



</div>

<script type="text/javascript">

  // $("#start_stream").hide()

  $('input[name="eyes"]').click(function(){
  
  var radioValue = $("input[name='eyes']:checked").val();

  setCookie("eyes", radioValue, 1024);

  location.reload();

            });

  $('#openImgUpload').click(function(){
    $('#imgupload').trigger('click');
});

$('#imgupload').on('change', function(){
    var formData = new FormData();
    formData.append('image', $(this)[0].files[0]);

    $.ajax({
        url: '/upload',
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        success: function(data){
            console.log('上传成功: ');
            console.log(data.filename);

            setCookie("bg_img", data.filename, 1024);

            var radioValue = $("input[name='options']:checked").val();

            setCookie("bg_con", radioValue, 1024);

            location.reload();


        }
    });
});

// 从 cookie 中获取保存的值
function getCookie(name) {
            const value = "; " + document.cookie;
            const parts = value.split("; " + name + "=");
            if (parts.length === 2) return parts.pop().split(";").shift();
        }
// 将选中的值写入 cookie
function setCookie(name, value, days) {
            const date = new Date();
            date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
            const expires = "expires=" + date.toUTCString();
            document.cookie = name + "=" + value + ";" + expires + ";path=/";
        }

// 控制背景色

// 读取 cookie 中的值并设置单选按钮的选中状态
const selectedValue = getCookie("bg_con");
if (selectedValue) {
    const radioButtons = document.getElementsByName("options");
    radioButtons.forEach(radio => {
      radio.checked = false;
        if (radio.value === selectedValue) {
            radio.checked = true;
        }
    });
}

// 读取 cookie 中的值并设置单选按钮的选中状态
const eyesValue = getCookie("eyes");
if (eyesValue) {
    const radioButtons = document.getElementsByName("eyes");
    radioButtons.forEach(radio => {

      radio.checked = false;
        if (radio.value == eyesValue) {
            radio.checked = true;
        }
    });
}

// 设置背景色

let radioValue = $("input[name='options']:checked").val();

if (getCookie("bg_color") === undefined) {$("#bg_color").val("gray");}else{$("#bg_color").val(getCookie("bg_color"));}

if(radioValue == "bg_color"){



$("#canvas").css("background-color",$("#bg_color").val());

}else{

    if (getCookie("bg_img") === undefined) {  



    }else{
      
      let imageUrl = "./uploads/"+getCookie("bg_img");
      $("#canvas").css("background-image", "url(" + imageUrl + ")");
      // $("#canvas").css("background-repeat","space");
    
    
    }

}


let eye_bool = true;

if (getCookie("eyes") === undefined) {}else{
  
  if (getCookie("eyes")=="false"){

    eye_bool = false;


  } 

}

// 数字人模型
var cubism4Model = './models/<%=model_path%>/<%=model_path%>.model3.json';

var selected_model = '<%-model_path%>';


var model_list = '<%-model_list%>';
model_list = JSON.parse(model_list);


var $select = $("#model_list");
$select.empty(); // 清空旧选项

// 遍历新选项列表并添加到select元素中
$.each(model_list, function(index, value) {

  if (value==selected_model){

    $select.append($("<option selected></option>").attr("value", value).text(value));

  }else{

    $select.append($("<option></option>").attr("value", value).text(value));

  }

});




// const cubism4Model = "./Hiyori/Hiyori.model3.json";

// const cubism4Model = "./March 7th/March 7th.model3.json";

const live2d = PIXI.live2d;


(async function main() {


  const app = new PIXI.Application({
    view: document.getElementById("canvas"),
    autoStart: true,
    resizeTo: window,
    transparent: true,
    backgroundAlpha: 0,
  });



  var models = await Promise.all([
    live2d.Live2DModel.from(cubism4Model,{ autoInteract: eye_bool })
  ]);

  models.forEach((model) => {
    app.stage.addChild(model);

    const scaleX = (innerWidth ) / model.width;
    const scaleY = (innerHeight ) / model.height;

    // fit the window
    model.scale.set(Math.min(scaleX, scaleY));

    model.y = innerHeight * 0.1;

    draggable(model);
    // addFrame(model);
    // addHitAreaFrames(model);
   // talk(model)
  });

  const model4 = models[0];


  model4.x = innerWidth  / 2;


  model4.on("hit", (hitAreas) => {
    if (hitAreas.includes("Body")) {
      model4.motion("Tap");
    }

    if (hitAreas.includes("Head")) {
      model4.expression();
    }
  });


  // 更新背景

  $("#update_bg").click(function() {
  
  var radioValue = $("input[name='options']:checked").val();

  setCookie("bg_con", radioValue, 1024);

  setCookie("bg_color", $("#bg_color").val(), 1024);



  location.reload();



});


  // 更新模型 

  $("#update_model").click(function() {
  
    axios.get('/edit_config',{
  params: {"model_path":$("#model_list").val()}
})
  .then(response => {
    // 处理成功响应
    console.log(response.data);
   location.reload();
  })
  .catch(error => {
    // 处理错误
    console.error(error);
    alert(error);
  });

});


  $("#play").click(function() {
  talk(model4,"./Keira.wav");
});


$("#stop").click(function() {

  model4.stopSpeaking();

});


$("#start").click(function() {

  console.log($("#text_talk").val());


  let text = $("#text_talk").val().trim();




  $("#start").prop("disabled", true);

  axios.defaults.timeout = 300000;

  axios.post($("#apiurl").val(), {
    text_lang: $("#text_lang").val(),
    ref_audio_path: $("#ref_audio_path").val(),
    prompt_lang: $("#prompt_lang").val(),
    prompt_text: $("#prompt_text").val(),
    text_split_method: $("#text_split_method").val(),
    batch_size: 10,
    media_type: 'wav',
    speed_factor: $("#speed_factor").val(),
    text: $("#text_talk").val()
}, {
    responseType: 'arraybuffer'
})
  .then(response => {

    console.log(response.data);
    // 将返回的音频数据转换为Blob对象
    const audioBlob = new Blob([response.data], { type: 'audio/wav' });

    console.log(audioBlob);

    // 创建一个URL对象用于播放音频
    const audioUrl = URL.createObjectURL(audioBlob);

    // 创建一个新的Audio对象并播放音频
    // const audio = new Audio(audioUrl);
    //audio.play();

    talk(model4,audioUrl);

    $("#start").prop("disabled",false);

  })
  .catch(error => {
    console.error('请求接口失败:', error);
    $("#start").prop("disabled",false);
  });


});



$("#start_talk").click(async function() {

console.log($("#text").val());


let text = $("#text").val().trim();


if(text == ""){

  alert("请输入聊天内容");
  return false;

}

$("#start_talk").prop("disabled", true);


const data = {

  "model":$("#model_name").val(),"stream":true,
  "messages": [
  {
      "role": "assistant",
      "content": $("#role_card").val()
    },
    {
      "role": "user",
      "content": $("#text").val()
    }
  ],
  "options": {
    "num_keep": 5,
    "num_predict": 100,
    "top_k": 20,
    "top_p": 0.9,
    "tfs_z": 0.5,
    "typical_p": 0.7,
    "repeat_last_n": 33,
    "temperature": parseFloat($("#temperature").val()),
    "repeat_penalty": 1.2,
    "presence_penalty": 1.5,
    "frequency_penalty": 1.0,
    "mirostat": 1,
    "mirostat_tau": 0.7,
    "mirostat_eta": 0.6,
    "penalize_newline": true,
    "numa": false,
    "num_predict": parseInt($("#tokens").val()),
    "num_thread": 8
  }

}

$("#text_talk").val(""); // 清空旧选项

  const response = await fetch($("#llmurl").val(), {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json"
},
});
const reader = response.body?.pipeThrough(new TextDecoderStream()).getReader();

while (true) {
let { done, value } = await reader.read();
if (done) {
console.log("***********************done");
$("#start_talk").prop("disabled",false);

$("#start").click();



break;
}
console.log("--------------------value");
try {
  value = JSON.parse(value);
  //console.log(value);
    // 在这里可以使用parsedValue变量
    const text = value.message.content;
$('#text_talk').val($('#text_talk').val() + text);

} catch (error) {
    console.error("JSON 解析出错: " + error);
}




}




});





$("#start_stream").click(async function() {

console.log($("#text_talk").val());


let text = $("#text").val().trim();



$("#start_stream").prop("disabled", true);


data = {text_lang: $("#text_lang").val(),
  ref_audio_path: $("#ref_audio_path").val(),
  prompt_lang: $("#prompt_lang").val(),
  prompt_text: $("#prompt_text").val(),
  text_split_method: $("#text_split_method").val(),
  batch_size: 1,
  media_type: 'ogg',
  speed_factor: $("#speed_factor").val(),
  text: $("#text_talk").val(),
  streaming_mode:"true"}


  const response = await fetch($("#apiurl").val(), {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json"
},
});
const reader = response.body.getReader();
const context = new (window.AudioContext || window.webkitAudioContext)();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log("***********************done");
$("#start_stream").prop("disabled", false);
break;
}
console.log("--------------------value");
console.log(value);

// 将返回的音频数据转换为Blob对象
const audioBlob = new Blob([value.buffer], { type: 'audio/ogg' });


// 创建一个URL对象用于播放音频
const audioUrl = URL.createObjectURL(audioBlob);

talk(model4,audioUrl);



// 将读取到的值添加到数组中
//chunks.push(value);
}
// // 合并所有读取到的二进制数据
// const audioBuffer = new Uint8Array(chunks.reduce((acc, val) => acc.concat(Array.from(val)), []));
// // 解码音频数据并播放
// context.decodeAudioData(audioBuffer.buffer).then(decodedData => {
// const source = context.createBufferSource();
// source.buffer = decodedData;
// source.connect(context.destination);
// source.start(0);
// }).catch(error => {
// console.error("Error decoding audio data:", error);
// });



});



})();


function talk(model,audio){

  
var audio_link =  audio;  //[Optional arg, can be null or empty] [relative or full url path] [mp3 or wav file] "./Keira.wav"
var volume = 1; // [Optional arg, can be null or empty] [0.0 - 1.0]
var expression = 8; // [Optional arg, can be null or empty] [index|name of expression]
var resetExpression = true; // [Optional arg, can be null or empty] [true|false] [default: true] [if true, expression will be reset to default after animation is over]
var crossOrigin = "anonymous"; // [Optional arg, to use not same-origin audios] [DEFAULT: null]

model.speak(audio_link, {volume: volume, expression:expression, resetExpression:resetExpression, crossOrigin: crossOrigin})

// Or if you want to keep some things default
model.speak(audio_link)
model.speak(audio_link, {volume: volume})
model.speak(audio_link, {expression:expression, resetExpression:resetExpression})



}




function draggable(model) {
  model.buttonMode = true;
  model.on("pointerdown", (e) => {
    model.dragging = true;
    model._pointerX = e.data.global.x - model.x;
    model._pointerY = e.data.global.y - model.y;
  });
  model.on("pointermove", (e) => {
    if (model.dragging) {
      model.position.x = e.data.global.x - model._pointerX;
      model.position.y = e.data.global.y - model._pointerY;
    }
  });
  model.on("pointerupoutside", () => (model.dragging = false));
  model.on("pointerup", () => (model.dragging = false));
}

function addFrame(model) {
  const foreground = PIXI.Sprite.from(PIXI.Texture.WHITE);
  foreground.width = model.internalModel.width;
  foreground.height = model.internalModel.height;
  foreground.alpha = 0.2;

  model.addChild(foreground);

  checkbox("Model Frames", (checked) => (foreground.visible = checked));
}

// function addHitAreaFrames(model) {
//   const hitAreaFrames = new live2d.HitAreaFrames();
//   hitAreaFrames.visible = true;
//   model.addChild(hitAreaFrames);

//   //checkbox("Hit Area Frames", (checked) => (hitAreaFrames.visible = checked));
// }

function checkbox(name, onChange) {
  const id = name.replace(/\W/g, "").toLowerCase();

  let checkbox = document.getElementById(id);

  if (!checkbox) {
    const p = document.createElement("p");
    p.innerHTML = `<input type="checkbox" id="${id}"> <label for="${id}">${name}</label>`;

    document.getElementById("control").appendChild(p);
    checkbox = p.firstChild;
  }

  checkbox.addEventListener("change", () => {
    onChange(checkbox.checked);
  });

  onChange(checkbox.checked);
}

</script>


<style>
#control{
  position: absolute;
  top:50px;
  left: 50px;
  color: white;
  font-size:18px;

}
</style>

</body>
</html>